热修复实现(一)
现在的热修复方案已经有很多了,例如alibaba
的dexposed、AndFix以及jasonross
的Nuwa等等。原来没有仔细去分析过也没想写这篇文章,但是之前InstantRun详解这篇文章中介绍了Android Studio Instant Run
的
实现原理,这不就是活生生的一个热修复吗? 随心情久久不能平复,我们何不用这种方式来实现。
方案有很多种,我就只说明下我想到的方式,也就是Instant Run
的方式:
分拆到不同的dex
中,然后通过classloader
来进行加载。但是在之前InstantRun
详解中只说到会通过内部的server
去判断该类是否有更新,如果有的话就去从新的dex
中加载该类,否则就从旧的dex
中加载,但这是如何实现的呢? 怎么去从不同的dex
中选择最新的那个来进行加载。
讲到这里需要先介绍一下ClassLoader
:
< A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.
在一般情况下,应用程序不需要创建ClassLoader
对象,而是使用当前环境已经存在的ClassLoader
。因为Java
的Runtime
环境在初始化时,其内部会创建一个ClassLoader
对象用于加载Runtime
所需的各种Java
类。
每个ClassLoader
必须有一个父类,在装载Class
文件时,子ClassLoader
会先请求父ClassLoader
加载该Class
文件,只有当其父ClassLoader
找不到该Class
文件时,子ClassLoader
才会继续装载该类,这是一种安全机制。
对于Android
的应用程序,本质上虽然也是用Java
开发,并且使用标准的Java
编译器编译出Class
文件,但最终的APK
文件中包含的却是dex
类型的文件。dex
文件是将所需的所有Class
文件重新打包,打包的规则不是简单的压缩,而是完全对Class
文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是dex
文件。由于dex
文件是一种经过优化的Class
文件,因此要加载这样特殊的Class
文件就需要特殊的类装载器,这就是DexClassLoader
,Android SDK
中提供的DexClassLoader
类就是出于这个目的。
总体来说,Android
默认主要有三个ClassLoader
:
BootClassLoader
: 系统启动时创建
Provides an explicit representation of the boot class loader. It sits at the head of the class loader chain and delegates requests to the VM's internal class loading mechanism.
PathClassLoader
: 可以加载/data/app
目录下的apk
,这也意味着,它只能加载已经安装的apk
;DexClassLoader
: 可以加载文件系统上的jar
、dex
、apk
;可以从SD
卡中加载未安装的apk
通过上面的分析知道,如果用多个dex
的话肯定会用到DexClassLoader
类,我们首先来看一下它的源码(这里
插一嘴,源码可以去googlesource中找):
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*
* <p>This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getDir(String, int)} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getDir("dex", 0);
* }</pre>
*
* <p><strong>Do not cache optimized classes on external storage.</strong>
* External storage does not provide access controls necessary to protect your
* application from code injection attacks.
*/
public class DexClassLoader extends BaseDexClassLoader {
/**
* Creates a {@code DexClassLoader} that finds interpreted and native
* code. Interpreted classes are found in a set of DEX files contained
* in Jar or APK files.
*
* <p>The path lists are separated using the character specified by the
* {@code path.separator} system property, which defaults to {@code :}.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; must not be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
注释说的太明白了,这里就不翻译了,但是我们并没有找到加载的代码,去它的父类中查找,
因为家在都是从loadClass()
方法中,所以我们去ClassLoader
类中看一下loadClass()
方法:
/**
* Loads the class with the specified name. Invoking this method is
* equivalent to calling {@code loadClass(className, false)}.
* <p>
* <strong>Note:</strong> In the Android reference implementation, the
* second parameter of {@link #loadClass(String, boolean)} is ignored
* anyway.
* </p>
*
* @return the {@code Class} object.
* @param className
* the name of the class to look for.
* @throws ClassNotFoundException
* if the class can not be found.
*/
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
/**
* Loads the class with the specified name, optionally linking it after
* loading. The following steps are performed:
* <ol>
* <li> Call {@link #findLoadedClass(String)} to determine if the requested
* class has already been loaded.</li>
* <li>If the class has not yet been loaded: Invoke this method on the
* parent class loader.</li>
* <li>If the class has still not been loaded: Call
* {@link #findClass(String)} to find the class.</li>
* </ol>
* <p>
* <strong>Note:</strong> In the Android reference implementation, the
* {@code resolve} parameter is ignored; classes are never linked.
* </p>
*
* @return the {@code Class} object.
* @param className
* the name of the class to look for.
* @param resolve
* Indicates if the class should be resolved after loading. This
* parameter is ignored on the Android reference implementation;
* classes are not resolved.
* @throws ClassNotFoundException
* if the class can not be found.
*/
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
// 先检查父ClassLoader是否已经家在过该类
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
// 调用DexClassLoader.findClass()方法。
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
上面会调用DexClassLoader.findClass()
方法,但是DexClassLoader
没有实现该方法,所以去它的父类BaseDexClassLoader
中看,接着看一下BaseDexClassLoader
的源码:
/**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
/** originally specified path (just used for {@code toString()}) */
private final String originalPath;
/** structured lists of path elements */
private final DexPathList pathList;
/**
* Constructs an instance.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; may be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 从DexPathList中找
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
在findClass()
方法中我们看到调用了DexPathList.findClass()
方法:
/**
* A pair of lists of entries, associated with a {@code ClassLoader}.
* One of the lists is a dex/resource path — typically referred
* to as a "class path" — list, and the other names directories
* containing native code libraries. Class path entries may be any of:
* a {@code .jar} or {@code .zip} file containing an optional
* top-level {@code classes.dex} file as well as arbitrary resources,
* or a plain {@code .dex} file (with no possibility of associated
* resources).
*
* <p>This class also contains methods to use these lists to look up
* classes and resources.</p>
*/
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk";
/** class definition context */
private final ClassLoader definingContext;
/** list of dex/resource (class path) elements */
// 把dex封装成一个数组,每个Element代表一个dex
private final Element[] dexElements;
/** list of native library directory elements */
private final File[] nativeLibraryDirectories;
// .....
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
// 遍历数组,拿到第一个就返回
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
}
从上面的源码中分析,我知道系统会把所有相关的dex
维护到一个数组中,然后在加载类的时候会从该数组中的第一个元素中取,然后返回。那我们只要保证将我们热修复后的dex
对应的Element
放到该数组的第一个位置就可以了,这样系统就会加载我们热修复的dex
中的类。
所以方案出来了,只要把有问题的类修复后,放到一个单独的dex
,然后把该Dex
转换成对应的Element
后再将该Element
插入到dexElements
数组的第一个位置就可以了。那该如何去将其插入到dexElements
数组的第一个位置呢?-- 暴力反射。
到这里我感觉初步的思路已经有了:
- 将补丁作为
dex
发布。 - 通过反射修改该
dex
所对应的Element
在数组中的位置。
但是我也想到肯定还会有类似下面的问题:
- 资源文件的处理
- 四大组件的处理
- 清单文件的处理
虽然我知道没有这么简单,但是我还是决定抱着不作不死的宗旨继续前行。
好了,demo
走起来。
怎么生成dex
文件呢? 这要讲过两部分:
.class
->.jar
:jar -cvf test.jar com/charon/instantfix_sample/MainActivity.class
.jar
->.dex
:dx --dex --output=target.jar test.jar
target.jar
就是包含.dex
的jar
包
生成好dex
后我们为了模拟先将其放到asset
目录下(实际开发中肯定要从接口中去下载,当然还会有一些版本号的判断等),然后就是将该dex
转换成
方案中采用的是MultiDex
,对其进行一部分改造,具体代码:
- 添加
dex
文件,并执行install
/**
* 添加apk包外的dex文件
* 自动执行install
* @param dexFile
*/
public static void addDexFileAutoInstall(Context context, List<File> dexFile,File optimizedDirectory) {
if (dexFile != null && !dexFile.isEmpty() &&!dexFiles.contains(dexFile)) {
dexFiles.addAll(dexFile);
LogUtil.d(TAG, "add other dexfile");
installDexFile(context,optimizedDirectory);
}
}
installDexFile
直接调用MultiDex
的installSecondaryDexes
方法
/**
* 添加apk包外的dex文件,
* @param context
*/
publicstatic void installDexFile(Context context, File optimizedDirectory){
if (checkValidZipFiles(dexFiles)) {
try {
installSecondaryDexes(context.getClassLoader(), optimizedDirectory, dexFiles);
} catch (IllegalAccessExceptione){
e.printStackTrace();
} catch (NoSuchFieldExceptione) {
e.printStackTrace();
} catch (InvocationTargetExceptione){
e.printStackTrace();
} catch (NoSuchMethodExceptione) {
e.printStackTrace();
} catch (IOExceptione) {
e.printStackTrace();
}
}
}
- 将
patch.dex
放在所有dex
最前面
private static voidexpandFieldArray(Object instance, String fieldName, Object[]extraElements) throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
Field jlrField = findField(instance, fieldName);
Object[]original = (Object[]) jlrField.get(instance);
Object[]combined = (Object[]) Array.newInstance(original.getClass().getComponentType(),original.length + extraElements.length);
// 将后来的dex放在前面,主dex放在最后。
System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
System.arraycopy(original, 0, combined, extraElements.length,original.length);
// 原始的dex合并,是将主dex放在前面,其他的dex依次放在后面。
//System.arraycopy(original, 0, combined, 0, original.length);
//System.arraycopy(extraElements, 0, combined, original.length,extraElements.length);
jlrField.set(instance, combined);
}
到此将patch.dex
放进了Element
,接下来的问题就是加载Class
,当加载patch.dex
中类的时候,会遇到一个问题,这个问题就是QQ
空间团队遇到的Class
的CLASS_ISPREVERIFIED
。具体原因是dvmResolveClass
这个方法对Class
进行了校验。判断这个要Resolve
的class
是否和其引用来自一个dex
。如果不是,就会遇到问题。
当引用这和被引用者不在同一个dex
中就会抛出异常,导致Resolve
失败。QQ
空间团队的方案是阻止所有的Class
类打上CLASS_ISPREVERIFIED
来逃过校验,这种方式其实是影响性能。
我们的方案是和QQ
团队的类似,但是和QQ
空间不同的是,我们将fromUnverifiedConstant
设置为true
,来逃过校验,达到补丁的路径。具体怎么实现呢?
要引用Cydia Hook
技术来hook Native dalvik
中dvmResolveClass
这个方法。有关Cydia Hook
技术请参考:
具体代码如下:
//指明要hook的lib :
MSConfig(MSFilterLibrary,"/system/lib/libdvm.so")
// 在初始化的时候进行hook
MSInitialize {
LOGD("Cydia Init");
MSImageRef image;
//载入lib
image = MSGetImageByName("/system/lib/libdvm.so");
if (image != NULL) {
LOGD("image is not null");
void *dexload=MSFindSymbol(image,"dvmResolveClass");
if(dexload != NULL) {
LOGD("dexloadis not null");
MSHookFunction(dexload, (void*)proxyDvmResolveClass, (void**)&dvmResolveClass_Proxy);
} else{
LOGD("errorfind dvmResolveClass");
}
}
}
// 在初始化的时候进行hook//保留原来的地址
ClassObject* (*dvmResolveClass_Proxy)(ClassObject* referrer, u4 classIdx, boolfromUnverifiedConstant);
// 新方法地址
static ClassObject* proxyDvmResolveClass(ClassObject* referrer, u4 classIdx,bool fromUnverifiedConstant) {
return dvmResolveClass_Proxy(referrer, classIdx,true);
}
说到此处,似乎已经是一个完整的方案了,但在实践中,会发现运行加载类的时候报preverified
错误,原来在DexPrepare.cpp
,将dex
转化成odex
的过程中,会在DexVerify.cpp
进行校验,
验证如果直接引用到的类和clazz
是否在同一个dex
,如果是,则会打上CLASS_ISPREVERIFIED
标志。通过在所有类(Application
除外,当时还没加载自定义类的代码)的构造函数插入一个对在单独的dex
的类的引用,就可以解决这个问题。空间使用了javaassist
进行编译时字节码插入。
所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED
标志。 最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下:
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
其中AntilazyLoad
类会被打包成单独的antilazy.dex
,这样当安装apk
的时候,classes.dex
内的类都会引用一个在不相同dex
中的AntilazyLoad
类,这样就防止了类被打上CLASS_ISPREVERIFIED
的标志了,只要没被打上这个标志的类都可以进行打补丁操作。 然后在应用启动的时候加载进来AntilazyLoad
类所在的dex
包必须被先加载进来,不然AntilazyLoad
类会被标记为不存在,即使后续加载了hack.dex
包,那么他也是不存在的,这样屏幕就会出现茫茫多的类AntilazyLoad
找不到的log
。 所以Application
作为应用的入口不能插入这段代码。(因为载入hack.dex
的代码是在Application
中onCreate
中执行的,如果在Application
的构造函数里面插入了这段代码,那么就是在hack.dex
加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志)
如何打包补丁包:
1. 空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有class
文件的md5
,还有一份mapping
混淆文件。
2. 在后续的版本中使用-applymapping
选项,应用正式版本的mapping
文件,然后计算编译完成后的class
文件的md5
和正式版本进行比较,把不相同的class
文件打包成补丁包。
备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包dex
,只需要把修改过的类的class
文件打包成patch dex
,然后放到sdcard
下,那么就会让改变的代码生效。
在Java
中,只有当两个实例的类名、包名以及加载其的ClassLoader
都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的ClassLoader
不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。
同一个Class
=相同的ClassName + PackageName + ClassLoader
以上问题在采用动态加载功能的开发中容易出现,请注意。
通过上面的分析,我们知道使用ClassLoader
动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的Java
程序,在Android
程序中使用动态加载主要有两个麻烦的问题:
Android
中许多组件类(如Activity、Service
等)是需要在Manifest
文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作;Res
资源是Android
开发中经常用到的,而Android
是把这些资源用对应的R.id
注册好,运行时通过这些ID
从Resource
实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到R.id
的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源ID根本和现有的Resource
实例中保存的资源ID
对不上;
说到底,抛开虚拟机的差别不说,一个Android
程序和标准的Java
程序最大的区别就在于他们的上下文环境(Context)
不同。Android
中,这个环境可以给程序提供组件需要用到的功能,也可以提供一些主题、Res
等资源,其实上面说到的两个问题都可以统一说是这个环境的问题,而现在的各种Android
动态加载框架中,核心要解决的东西也正是“如何给外部的新类提供上下文环境”的问题。
DexClassLoader
的使用方法一般有两种:
- 从已安装的
apk
中读取dex
- 从
apk
文件中读取dex
假如有两个APK
,一个是宿主APK
,叫作HOST
,一个是插件APK
,叫作Plugin
。Plugin
中有一个类叫PluginClass
,代码如下:
public class PluginClass {
public PluginClass() {
Log.d("JG","初始化PluginClass");
}
public int function(int a, int b){
return a+b;
}
}
现在如果想调用插件APK
中PluginClass
内的方法,应该怎么办?
从已安装的apk中读取dex
先来看第一种方法,这种方法必须建一个Activity
,在清单文件中配置Action
.
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="com.maplejaw.plugin"/>
</intent-filter>
</activity>
然后在宿主APK
中如下使用:
/**
* 这种方式用于从已安装的apk中读取,必须要有一个activity,且需要配置ACTION
*/
private void useDexClassLoader(){
//创建一个意图,用来找到指定的apk
Intent intent = new Intent("com.maplejaw.plugin");
//获得包管理器
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
if(resolveinfoes.size()==0){
return;
}
//获得指定的activity的信息
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
//获得包名
String packageName = actInfo.packageName;
//获得apk的目录或者jar的目录
String apkPath = actInfo.applicationInfo.sourceDir;
//dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己
//目录下的文件
String dexOutputDir = getApplicationInfo().dataDir;
//native代码的目录
String libPath = actInfo.applicationInfo.nativeLibraryDir;
//创建类加载器,把dex加载到虚拟机中
DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutputDir, libPath,
this.getClass().getClassLoader());
//利用反射调用插件包内的类的方法
try {
Class<?> clazz = calssLoader.loadClass(packageName+".PluginClass");
Object obj = clazz.newInstance();
Class[] param = new Class[2];
param[0] = Integer.TYPE;
param[1] = Integer.TYPE;
Method method = clazz.getMethod("function", param);
Integer ret = (Integer)method.invoke(obj, 12,34);
Log.d("JG", "返回的调用结果为:" + ret);
} catch (Exception e) {
e.printStackTrace();
}
}
我们安装完两个APK
后,在宿主中就可以直接调用,调用示例如下。
public void btnClick(View view){
useDexClassLoader();
}
从apk文件中读取dex
这种方法由于并不需要安装,所以不需要通过Intent
从activity
中解析信息。换言之,这种方法不需要创建Activity
。无需配置清单文件。我们只需要打包一个apk
,然后放到SD
卡中即可。
核心代码如下:
//apk路径
String path=Environment.getExternalStorageDirectory().getAbsolutePath()+"/1.apk";
private void useDexClassLoader(String path){
File codeDir=getDir("dex", Context.MODE_PRIVATE);
//创建类加载器,把dex加载到虚拟机中
DexClassLoader calssLoader = new DexClassLoader(path, codeDir.getAbsolutePath(), null,
this.getClass().getClassLoader());
//利用反射调用插件包内的类的方法
try {
Class<?> clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass");
Object obj = clazz.newInstance();
Class[] param = new Class[2];
param[0] = Integer.TYPE;
param[1] = Integer.TYPE;
Method method = clazz.getMethod("function", param);
Integer ret = (Integer)method.invoke(obj, 12,21);
Log.d("JG", "返回的调用结果为: " + ret);
} catch (Exception e) {
e.printStackTrace();
}
动态加载的几个关键问题:
资源访问:无法找到某某
id
所对应的资源 因为将apk
加载到宿主程序中去执行,就无法通过宿主程序的Context
去取到apk
中的资源,比如图片、文本等,这是很好理解的,因为apk
已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文, 用别人的Context
是无法得到自己的资源的- 解决方案一:插件中的资源在宿主程序中也预置一份;
缺点:增加了宿主apk
的大小;在这种模式下,每次发布一个插件都需要将资源复制到宿主程序中,这意味着每发布一个插件都要更新一下宿主程序; - 解决方案二:将插件中的资源解压出来,然后通过文件流去读取资源;
缺点:实际操作起来还是有很大难度的。首先不同资源有不同的文件流格式,比如图片、XML等,其次针对不同设备加载的资源可能是不一样的,如何选择合适的资源也是一个需要解决的问题; 实际解决方案:Activity
中有一个叫mBase
的成员变量,它的类型就是ContextImpl
。注意到Context
中有如下两个抽象方法,看起来是和资源有关的,实际上Context
就是通过它们来获取资源的。这两个抽象方法的真正实现在ContextImpl
中;
- 解决方案一:插件中的资源在宿主程序中也预置一份;
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
具体实现:
protected void loadResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
加载资源的方法是通过反射,通过调用AssetManager
中的addAssetPath
方法,我们可以将一个apk
中的资源加载到Resources
对象中,由于addAssetPath
是隐藏API
我们无法直接调用,所以只能通过反射。
@hide
public final int addAssetPath(String path) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
Activity
生命周期的管理:反射方式和接口方式。
反射的方式很好理解,首先通过Java
的反射去获取Activity
的各种生命周期方法,比如onCreate
、onStart
、onResume
等,然后在代理Activity
中去调用插件Activity
对应的生命周期方法即可:
缺点:一方面是反射代码写起来比较复杂,另一方面是过多使用反射会有一定的性能开销。
反射方式
@Override
protected void onResume() {
super.onResume();
Method onResume = mActivityLifecircleMethods.get("onResume");
if (onResume != null) {
try {
onResume.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onPause() {
Method onPause = mActivityLifecircleMethods.get("onPause");
if (onPause != null) {
try {
onPause.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
super.onPause();
}
接口方式
public interface DLPlugin {
public void onStart();
public void onRestart();
public void onActivityResult(int requestCode, int resultCode, Intent
data);
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onCreate(Bundle savedInstanceState);
public void setProxy(Activity proxyActivity, String dexPath);
public void onSaveInstanceState(Bundle outState);
public void onNewIntent(Intent intent);
public void onRestoreInstanceState(Bundle savedInstanceState);
public boolean onTouchEvent(MotionEvent event);
public boolean onKeyUp(int keyCode, KeyEvent event);
public void onWindowAttributesChanged(LayoutParams params);
public void onWindowFocusChanged(boolean hasFocus);
public void onBackPressed();
…
}
代理Activity
中只需要按如下方式即可调用插件Activity
的生命周期方法,这就完成了插件Activity
的生命周期的管理;插件Activity
需要实现DLPlugin
接口:
@Override
protected void onStart() {
mRemoteActivity.onStart();
super.onStart();
}
@Override
protected void onRestart() {
mRemoteActivity.onRestart();
super.onRestart();
}
@Override
protected void onResume() {
mRemoteActivity.onResume();
super.onResume();
}
- Google Instant app
- 微信热补丁实现
- 多dex分拆
- QQ空间热修复方案
- Android dex分包方案
- 类加载器DexClassLoader
- 基于cydia Hook在线热修复补丁方案
- Android 热补丁动态修复框架小结
- 美团Android DEX自动拆包及动态加载简介
- 插件化从放弃到捡起
- 无需Root也能使用Xposed!
- 当你准备开发一个热修复框架的时候,你需要了解的一切
- 邮箱 :[email protected]
- Good Luck!